﻿using System;
using System.Data;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Xsl;
using System.Xml.XPath;
using Aspose.Words;
using Aspose.Words.Fields;
using Aspose.Words.Tables;
using Aspose.Words.Drawing;
using System.IO;

namespace AntsCode.WordsExtensions
{
    /// <summary>
    /// AdvancedMailMerge http://advancedmailmerge.codeplex.com
    /// Performs advanced mail merge functions on an Aspose.Words Document object
    /// 2009 Anthony Super http://antscode.blogspot.com
    /// </summary>
    public class AdvancedMailMerge
    {
        private List<MergeError> ErrorList;
        private List<MergeRegion> RegionList;
        private List<MergeRegionData> RegionData;

        public AdvancedMailMerge()
        {
            InitData();
        }

        public void InitData()
        {
            // Initialise region data
            this.RegionList = new List<MergeRegion>();
            this.RegionData = new List<MergeRegionData>();
        }

        /// <summary>
        /// Loads an Aspose.Words Document object for merging.
        /// The document is validated when loaded, and the Errors property is populated with any 
        /// errors found in the document.
        /// The Regions property is also populated with any regions found in the document.
        /// </summary>
        /// <param name="document">An Aspose.Words Document object.</param>
        public void Load(Document document)
        {
            this.Document = document;

            // Initialise error list
            this.ErrorList = new List<MergeError>();

            // Start the recursive function to get all regions in the document
            this.GetRegionsForLevel(this.Document.Document, this.RegionList, null);
        }

        public void AddRegionData(DataTable data)
        {
            this.RegionData.Add(new MergeRegionData(data));
        }

        public void AddRegionData(DataTable data, string name)
        {
            this.RegionData.Add(new MergeRegionData(data, name));
        }

        public void AddRegionData(DataTable data, string name, string idColumn)
        {
            this.RegionData.Add(new MergeRegionData(data, name, idColumn));
        }

        public void AddRegionData(DataTable data, string name, string idColumn, 
            string parentIdColumn)
        {
            this.RegionData.Add(new MergeRegionData(data, name, idColumn, parentIdColumn));
        }

        public void AddRegionData(DataTable data, string name, string idColumn,
            string parentIdColumn, string sortBy)
        {
            this.RegionData.Add(new MergeRegionData(data, name, idColumn, parentIdColumn, sortBy));
        }

        public void ClearAllData()
        {
            this.DocumentData = null;
            this.RegionData.Clear();
        }

        /// <summary>
        /// Merges the document provided in the Load method with the data provided in the 
        /// AddRegionData methods.
        /// Note that if the document contains errors, Merge will throw an exception. You should 
        /// always check the Errors property before calling this method.
        /// </summary>
        public void Merge()
        {
            // If there are any errors then we cannot perform a merge
            if (this.ErrorList.Count > 0)
            {
                throw (new ApplicationException("Document contains errors. Always check the " + 
                    "Errors property before calling Merge()."));
            }

            // Merge each region
            Node insertAfter = null;

            foreach (MergeRegion region in this.Regions)
            {
                insertAfter = region.End;
                this.MergeRegion(region, null, insertAfter.ParentNode, insertAfter, null, null);

                // Once the region is merged, it can be removed from the tree
                this.RemoveRegion(region);
            }

            // Once all regions are merged, merged any document-level data
            this.MergeDocumentData();

            // Remove any empty paragraphs
            NodeList fields = this.Document.SelectNodes("//Paragraph[Run='REMOVE_PARA']");

            foreach (Node field in fields)
            {
                field.Remove();
            }
        }

        public IEnumerable<MergeError> Errors
        {
            get
            {
                return ErrorList;
            }
        }

        public IEnumerable<MergeRegion> Regions
        {
            get
            {
                return RegionList;
            }
        }

        public Document Document
        {
            get;
            private set;
        }

        public DataRow DocumentData
        {
            private get;
            set;
        }

        public event MergeFieldEventHandler MergeField;
        public event MergeImageFieldEventHandler MergeImageField;

        private Node MergeRegion(MergeRegion region, CompositeNode sourceParent, 
            CompositeNode destParent, Node insertAfter, DataRow parentDataRow, DataRow dataRow)
        {
            switch(region.Type)
            {
                case MergeRegionType.Table:
                    insertAfter = this.MergeTableRegion(region, sourceParent, destParent, 
                        insertAfter, parentDataRow);
                    break;

                case MergeRegionType.Conditional:
                    insertAfter = this.MergeConditionalRegion(region, sourceParent, destParent,
                        insertAfter, parentDataRow, dataRow);
                    break;
            }

            return insertAfter;
        }

        private Node MergeTableRegion(MergeRegion region, CompositeNode sourceParent,
            CompositeNode destParent, Node insertAfter, DataRow parentDataRow)
        {
            // Do we have data for this region?
            MergeRegionData regionData = this.GetRegionData(region.Name);

            if (regionData != null)
            {
                DataRow[] dataRows;
                string selectStatement = "";

                // Does this region have a filter?
                if (region.Filter != null)
                {
                    selectStatement = region.Filter;
                }

                // Do we also need to filter by a parent id?
                if (parentDataRow != null && regionData.ParentIdColumn != null)
                {
                    if (selectStatement != "")
                    {
                        selectStatement += " AND ";
                    }

                    int parentDataRowId = -1;

                    if(!Convert.IsDBNull(parentDataRow[regionData.ParentIdColumn]))
                    {
                        parentDataRowId = (int)parentDataRow[regionData.ParentIdColumn];
                    }

                    selectStatement += regionData.ParentIdColumn + " = " + parentDataRowId;
                }

                if (regionData.SortBy != null)
                {
                    dataRows = regionData.Data.Select(selectStatement, regionData.SortBy);
                }
                else
                {
                    dataRows = regionData.Data.Select(selectStatement);
                }

                // Iterate through each data row and merge
                foreach (DataRow dataRow in dataRows)
                {
                    // Insert a copy of the region nodes
                    insertAfter = this.InsertRegionNodes(region, dataRow, sourceParent,
                        destParent, insertAfter, dataRow);
                }
            }

            return insertAfter;
        }

        private MergeRegionData GetRegionData(string regionName)
        {
            return (from d in this.RegionData
                    where d.Name == regionName
                    select d).FirstOrDefault();
        }

        private Node MergeConditionalRegion(MergeRegion region, CompositeNode sourceParent,
            CompositeNode destParent, Node insertAfter, DataRow parentDataRow, DataRow dataRow)
        {
            // Construct a DataTable that combines the region data with global data
            DataTable data = this.GetConditionalDataTable(dataRow);
           
            // Now that we've constructed the DataTable, we can perform a select query
            DataRow[] result = data.Select(region.Filter);

            if (result.Length > 0)
            {
                // Condition is true, insert a copy of the region nodes
                insertAfter = this.InsertRegionNodes(region, dataRow, sourceParent,
                    destParent, insertAfter, parentDataRow);
            }
            else
            {
                // Condition is false, do nothing
            }

            return insertAfter;
        }

        private DataTable GetConditionalDataTable(DataRow dataRow)
        {
            DataTable data = new DataTable();
            DataRow row = null;

            // Add region data if it exists
            if (dataRow != null)
            {
                data = dataRow.Table.Clone();
                data.ImportRow(dataRow);

                // If the original data row was not added to the data table (e.g. in ValidateRegionFilter)
                // then ImportRow will not do anything.
                if (data.Rows.Count > 0)
                {
                    row = data.Rows[0];
                }
            }

            // Add global data if it exists
            if (this.DocumentData != null)
            {
                // Add document data columns
                foreach (DataColumn column in this.DocumentData.Table.Columns)
                {
                    data.Columns.Add(new DataColumn("Document." + column.ColumnName, column.DataType));
                }

                if (row == null)
                {
                    row = data.NewRow();
                    row = PopulateDummyRow(row);
                    data.Rows.Add(row);
                }

                // Populate document data
                foreach (DataColumn column in this.DocumentData.Table.Columns)
                {
                    data.Rows[0]["Document." + column.ColumnName] = this.DocumentData[column.ColumnName];
                }
            }

            return data;
        }

        private DataRow PopulateDummyRow(DataRow row)
        {
            foreach (DataColumn column in row.Table.Columns)
            {
                if (column.AllowDBNull == false)
                {
                    switch (column.DataType.ToString())
                    {
                        case "System.String":
                            row[column.ColumnName] = "";
                            break;
                        case "System.DateTime":
                            row[column.ColumnName] = new DateTime();
                            break;
                        case "System.DateTimeOffset":
                            row[column.ColumnName] = new DateTimeOffset();
                            break;
                        case "System.Boolean":
                            row[column.ColumnName] = false;
                            break;
                        default:
                            row[column.ColumnName] = 0;
                            break;
                    }
                }
            }

            return row;
        }

        private void MergeDocumentData()
        {
            // Get all remaining merge fields
            NodeList fields = this.Document.SelectNodes("//FieldStart");
            MergeField mergeField;

            if (this.DocumentData != null)
            {
                foreach (Node field in fields)
                {
                    mergeField = this.GetMergeField((FieldStart)field);

                    if (mergeField != null)
                    {
                        this.SetMergeField(mergeField, this.DocumentData);
                    }
                }
            }
        }

        private Node InsertRegionNodes(MergeRegion region, DataRow dataRow, CompositeNode sourceParent,
            CompositeNode destParent, Node insertAfter, DataRow parentDataRow)
        {
            MergeRegion subRegion;
            Node newNode;
            Node skipUntilNode = null;
            IEnumerable<Node> sourceNodes;
            List<Node> insertedNodes = new List<Node>();

            if (sourceParent == null)
            {
                sourceNodes = region.Nodes;
            }
            else
            {
                sourceNodes = this.GetChildNodes(sourceParent);
            }

            foreach (Node node in sourceNodes)
            {    
                // Is this node the start of another region?
                subRegion = this.GetRegionFromStartNode(region.ChildRegions, node);

                if (subRegion != null)
                {
                    // Yes, so merge the sub region
                    insertAfter = this.MergeRegion(subRegion, null, destParent,
                        insertAfter, parentDataRow, dataRow);

                    // Tell the current loop to skip all nodes until the end of this region
                    if(subRegion.Start != subRegion.End) 
                    {
                        skipUntilNode = subRegion.End;
                    }
                }
                else
                {
                    if (skipUntilNode == null)
                    {
                        // Create a copy of the node to add to the document
                        newNode = node.Clone(false);

                        // Insert the node into the tree
                        if (insertAfter == null)
                        {
                            destParent.PrependChild(newNode);
                        }
                        else
                        {
                            insertAfter.ParentNode.InsertAfter(newNode, insertAfter);
                        }

                        // Does this node have children?
                        if (node.IsComposite)
                        {
                            // Yes, so recurse
                            this.InsertRegionNodes(region, dataRow, (CompositeNode)node,
                                (CompositeNode)newNode, null, parentDataRow);
                        }

                        insertedNodes.Add(newNode);
                        insertAfter = newNode;
                    }

                    // If the current node is the skip-until-node, then clear it
                    if (skipUntilNode == node)
                    {
                        skipUntilNode = null;
                    }
                }
            }

            // Once all nodes are added, set any merge fields
            this.MergeNodes(insertedNodes, dataRow);

            return insertAfter;
        }

        private MergeRegion GetRegionFromStartNode(IEnumerable<MergeRegion> regions, Node startNode)
        {
            MergeRegion foundRegion = null;

            foreach (MergeRegion region in regions)
            {
                if (region.Start == startNode)
                {
                    foundRegion = region;
                    break;
                }
            }

            return foundRegion;
        }

        private void MergeNodes(List<Node> nodes, DataRow rowData)
        {
            MergeField mergeField;

            foreach (Node node in nodes)
            {
                if (node.GetType() == typeof(FieldStart))
                {
                    mergeField = this.GetMergeField((FieldStart)node);
                    this.SetMergeField(mergeField, rowData);
                }
            }
        }

        private void SetMergeField(MergeField mergeField, DataRow rowData)
        {
            string mergeValue = "";

            // Ensure this fields has not already been merged
            if (!mergeField.Merged)
            {
                // Ensure this is a valid merge field
                if (mergeField.Name != null)
                {
                    // Does a data column exist for this merge field?
                    if (rowData != null)
                    {
                        if (rowData.Table.Columns.Contains(mergeField.Name))
                        {
                            mergeValue = Convert.ToString(rowData[mergeField.Name]);

                        }
                    }
                    else
                    {
                        // No, so look for the data at the document level
                        if (this.DocumentData.Table.Columns.Contains(mergeField.Name))
                        {
                            mergeValue = Convert.ToString(this.DocumentData[mergeField.Name]);
                        }
                    }

                    // Format the value
                    mergeValue = MergeFieldFormatter.FormatValue(mergeField, mergeValue);

                    // Is this an image merge field?
                    if (mergeField.Name.StartsWith("Image:", StringComparison.CurrentCultureIgnoreCase))
                    {
                        this.SetImageMergeField(mergeField, mergeValue, rowData);
                    }
                    else if (!string.IsNullOrEmpty(mergeValue) && mergeValue.Contains("\\rtf"))
                    {
                        this.SetRTFMergeField(mergeField, mergeValue);
                    }
                    else
                    {
                        this.SetTextMergeField(mergeField, mergeValue, rowData);
                    }
                }
            }
        }

        private void SetImageMergeField(MergeField mergeField, string mergeValue, DataRow rowData)
        {
            // Has an event handler been provided?
            if (this.MergeImageField != null)
            {
                MergeImageFieldEventArgs args = new MergeImageFieldEventArgs();

                args.Document = this.Document;
                args.Field = mergeField;
                args.Row = rowData;
                args.FieldName = mergeField.Name.Replace("Image:", "");
                args.FieldValue = mergeValue;

                this.MergeImageField(this, args);

                Shape shape = new Shape(this.Document, ShapeType.Image);
                shape.WrapType = WrapType.Inline;

                // Has an image file name been provided by the handler?
                if (args.ImageFileName != null)
                {
                    shape.ImageData.SetImage(args.ImageFileName);
                }
                else if (args.Image != null)
                {
                    shape.ImageData.SetImage(args.Image);
                }

                if (args.ImageFileName != null || args.Image != null)
                {
                    this.ResizeImage(args, shape);

                    // Replace the merge field with the image
                    mergeField.Start.ParentNode.InsertBefore(shape, mergeField.Start);
                }

                this.RemoveMergeField(mergeField);
            }
        }

        private void SetTextMergeField(MergeField mergeField, string mergeValue, DataRow rowData)
        {
            Run run;

            // Word only supports \r for line breaks
            mergeValue = mergeValue.Replace("\r\n", "\r");
            mergeValue = mergeValue.Replace("\n", "\r");

            // Clear all run values within the merge field
            foreach (Node node in mergeField.Nodes)
            {
                if(node.GetType() == typeof(Run))
                {
                    run = (Run)node;
                    run.Text = "";
                }
            }

            // Has an event handler been provided?
            if (this.MergeField != null)
            {
                MergeFieldEventArgs args = new MergeFieldEventArgs();

                args.Document = this.Document;
                args.Field = mergeField;
                args.Row = rowData;
                args.FieldName = mergeField.Name;
                args.FieldValue = mergeValue;
                args.TableName = rowData.Table.TableName;
                args.Text = mergeValue;

                this.MergeField(this, args);

                mergeValue = args.Text;
            }

            // Change the value of the third run node
            // This node contains all text formatting
            run = (Run)mergeField.Nodes.ElementAt(3);
            run.Text = mergeValue;

            if (run.Text == "")
            {
                // The merged value is blank. Is this the only merge field in this paragraph?
                CompositeNode parentNode = mergeField.Start.ParentNode;

                if (parentNode.GetType() == typeof(Paragraph) && parentNode.ChildNodes.Count == mergeField.Nodes.Count())
                {
                    // Yes, so flag the paragraph to be removed
                    run.Text = "REMOVE_PARA";
                }
                else
                {
                    // Blank values cause Aspose.Words 2.0.50727 to crash when generating a PDF 
                    // (don't know why) so here replace empty values with a space.
                    run.Text = " ";
                }
            }

            mergeField.Merged = true;
        }

        private void SetRTFMergeField(MergeField mergeField, string mergeValue)
        {
            if (!string.IsNullOrEmpty(mergeValue) && mergeValue.Contains("\\rtf"))
            {
                Node parentNode = mergeField.Nodes.ElementAt(3).ParentNode;
                Run run;

                // Clear all run values within the merge field
                foreach (Node node in mergeField.Nodes)
                {
                    if (node.GetType() == typeof(Run))
                    {
                        run = (Run)node;
                        run.Text = "";
                    }
                }

                // RTF documents uses ASCII encoding.
                // Find ANSI codepage (\\ansicpg1252).
                string CodePage = "", WindowsCodePage = "";
                Regex re = new Regex(@"\\ansicpg\d+");
                Match m = re.Match(mergeValue);
                if (m.Success)
                {
                    // Get codepage (1252)
                    Regex reCodepage = new Regex(@"\d+");
                    Match mCodePage = reCodepage.Match(m.Value);
                    if (mCodePage.Success)
                    {
                        CodePage = mCodePage.Value;
                    }
                }

                byte[] rtfBytes = null;
                try
                {
                    if (!string.IsNullOrEmpty(CodePage))
                    {
                        WindowsCodePage = "Windows-" + CodePage; // "Windows-1252"
                        rtfBytes = Encoding.GetEncoding(WindowsCodePage).GetBytes(mergeValue);
                    }
                }
                catch (System.ArgumentException)
                {
                    // GetEncoding failed
                }
                finally
                {
                    if (rtfBytes == null || rtfBytes.Length == 0)
                    {
                        rtfBytes = Encoding.UTF8.GetBytes(mergeValue);
                    }
                }

                // Insert RTF document into destination document
                MemoryStream rtfStream = new MemoryStream(rtfBytes);
                Document rtfDoc = new Document(rtfStream);
                InsertDocument(parentNode, rtfDoc);
            }
        }

        /// <summary>
        /// Inserts content of the external document after the specified node.
        /// Section breaks and section formatting of the inserted document are ignored.
        /// </summary>
        /// <param name="insertAfterNode">Node in the destination document after which the content 
        /// should be inserted. This node should be a block level node (paragraph or table).</param>
        /// <param name="srcDoc">The document to insert.</param>
        public void InsertDocument(Node insertAfterNode, Document srcDoc)
        {
            // Make sure that the node is either a pargraph or table.
            if ((!insertAfterNode.NodeType.Equals(NodeType.Paragraph)) &
              (!insertAfterNode.NodeType.Equals(NodeType.Table)))
            {
                throw new ArgumentException("The destination node should be either a paragraph or table.");
            }

            // We will be inserting into the parent of the destination paragraph.
            CompositeNode dstStory = insertAfterNode.ParentNode;

            // This object will be translating styles and lists during the import.
            NodeImporter importer = new NodeImporter(srcDoc, insertAfterNode.Document, ImportFormatMode.KeepSourceFormatting);

            // Loop through all sections in the source document.
            foreach (Section srcSection in srcDoc.Sections)
            {
                // Loop through all block level nodes (paragraphs and tables) in the body of the section.
                foreach (Node srcNode in srcSection.Body)
                {
                    // Let's skip the node if it is a last empty paragarph in a section.
                    if (srcNode.NodeType.Equals(NodeType.Paragraph))
                    {
                        Paragraph para = (Paragraph)srcNode;

                        if (para.IsEndOfSection && !para.HasChildNodes)
                            continue;
                    }

                    // This creates a clone of the node, suitable for insertion into the destination document.
                    Node newNode = importer.ImportNode(srcNode, true);

                    // Insert new node after the reference node.
                    dstStory.InsertAfter(newNode, insertAfterNode);
                    insertAfterNode = newNode;
                }
            }
        }

        private Shape ResizeImage(MergeImageFieldEventArgs args, Shape shape)
        {
            double sourceWidth = shape.ImageData.ImageSize.WidthPoints;
            double sourceHeight = shape.ImageData.ImageSize.HeightPoints;
            double destWidth = sourceWidth;
            double destHeight = sourceHeight;
            double nPercent = 0;
            double nPercentW = 0;
            double nPercentH = 0;

            if (args.WidthPoints > 0)
            {
                destWidth = args.WidthPoints;
            }

            if (args.HeightPoints > 0)
            {
                destHeight = args.HeightPoints;
            }

            if (args.MaintainAspectRatio)
            {
                nPercentW = (destWidth / sourceWidth);
                nPercentH = (destHeight / sourceHeight);

                if (nPercentH < nPercentW)
                {
                    nPercent = nPercentH;
                }
                else
                {
                    nPercent = nPercentW;
                }

                destWidth = (sourceWidth * nPercent);
                destHeight = (sourceHeight * nPercent);
            }

            shape.Width = destWidth;
            shape.Height = destHeight;

            return shape;
        }

        private List<Node> GetChildNodes(CompositeNode parentNode)
        {
            List<Node> nodes = new List<Node>();

            foreach (Node node in parentNode.ChildNodes)
            {
                nodes.Add(node);
            }

            return nodes;
        }

        private void RemoveRegion(MergeRegion region)
        {
            foreach (Node node in region.Nodes)
            {
                node.Remove();
            }
        }

        private void RemoveMergeField(MergeField mergeField)
        {
            CompositeNode parentNode = mergeField.Start.ParentNode;

            foreach (Node node in mergeField.Nodes)
            {
                node.Remove();
            }

            // Is the parent node now an empty paragraph?
            if (parentNode.GetType() == typeof(Paragraph) && parentNode.ChildNodes.Count == 0)
            {
                // Yes, so flag it to be removed
                Run run = new Run(this.Document, "REMOVE_PARA");
                parentNode.AppendChild(run);
            }
        }

        private void GetRegionsForLevel(CompositeNode parentNode, List<MergeRegion> regionList, MergeRegion parentRegion)
        {
            // A region is defined by 2 merge fields with the syntax TableStart:MyTable and 
            // TableEnd:MyTable, where MyTable is the name of the DataTable to merge with.
            foreach(Node node in parentNode.ChildNodes)
            {
                // Is this Node a Field?
                if (node.GetType() == typeof(FieldStart))
                {
                    this.AddRegion((FieldStart)node, regionList, parentRegion);
                }
                else if(node.IsComposite)
                {
                    // This is not a field, so recurse to look for child fields
                    this.GetRegionsForLevel((CompositeNode)node, regionList, parentRegion);
                }
            }
        }

        private void AddRegion(FieldStart fieldStart, List<MergeRegion> regionList, MergeRegion parentRegion)
        {
            MergeField regionStart = this.GetMergeField(fieldStart);
            MergeRegion region = this.GetRegion(regionStart, parentRegion);

            // Is this field a region?
            if (region != null)
            {
                // Add the region to the object list
                regionList.Add(region);

                // This region may contain child regions, so loop through each node
                foreach (Node node in region.Nodes)
                {
                    if (node.GetType() == typeof(FieldStart))
                    {
                        AddRegion((FieldStart)node, (List<MergeRegion>)region.ChildRegions, region);
                    }
                    else if (node.IsComposite)
                    {
                        // Not a region, but may contain children that are regions
                        this.GetRegionsForLevel((CompositeNode)node,
                           (List<MergeRegion>)region.ChildRegions, region);
                    }
                }
            }
        }

        private MergeField GetMergeField(FieldStart fieldStart)
        {
            MergeField mergeField = new MergeField();

            // A merge field is not a single node, but 5 separate nodes:
            // 1. FieldStart
            // 2. Run - this contains the value of the field
            // 3. FieldSeparator
            // 4. Run - this contains the 'friendly name' of the field
            // 5. FieldEnd

            // Check that this is a valid merge field
            if (this.IsMergeField(fieldStart))
            {
                // Get the merge field name & switches
                string name;
                IEnumerable<MergeFieldSwitch> switches;
                ParseMergeFieldValue(fieldStart.NextSibling.GetText(), out name, out switches);

                mergeField.Start = fieldStart;
                mergeField.Name = name;
                mergeField.Switches = switches;

                // Add all merge field nodes
                mergeField.Nodes = new List<Node>();

                List<Node> mergeFieldNodes = new List<Node>();
                Node currentNode = fieldStart;

                while (currentNode.GetType() != typeof(FieldEnd))
                {
                    mergeFieldNodes.Add(currentNode);
                    currentNode = currentNode.NextSibling;
                }

                // Add the end node
                mergeFieldNodes.Add(currentNode);
                mergeField.End = (FieldEnd)currentNode;
                mergeField.Nodes = mergeFieldNodes;
            }

            return mergeField;
        }

        private bool IsMergeField(FieldStart fieldStart)
        {
            bool validMergeField = true;

            // Ensure that this is a merge field and not a different field
            if (fieldStart.NextSibling != null)
            {
                string fieldValue = fieldStart.NextSibling.GetText().Trim();

                if (!fieldValue.StartsWith("MERGEFIELD", StringComparison.CurrentCultureIgnoreCase))
                {
                    validMergeField = false;
                }
            }
            else
            {
                validMergeField = false;
            }

            return validMergeField;
        }

        private void ParseMergeFieldValue(string value, out string name, 
            out IEnumerable<MergeFieldSwitch> switches)
        {
            name = null;
            switches = null;
            
            // The structure of a Word merge field value is as follows:
            // MERGEFIELD <<FieldName>> <<Switches>>
            // e.g MERGEFIELD  FirstName  \* MERGEFORMAT

            // Split the value to get the switches
            string[] components = value.Trim().Split(@"\".ToCharArray());

            // The first component contains the word MERGEFIELD, 
            // followed by the merge field label, separated by a space

            // Set the name
            name = components[0].Replace("MERGEFIELD", "").Trim();

            // Set the switches
            switches = this.ParseMergeFieldSwitches(components);
        }

        private void ParseRegionValue(string value, out string name, out string filter)
        {
            name = null;
            filter = null;

            // The structure of a Region is as follows:
            // MERGEFIELD <<RegionName>> <<Filter>>
            // e.g MERGEFIELD IfStart:Taxable Taxable = 1

            // Split the value to get the switches
            string[] components = value.Replace("\"", "").Trim().Split(":".ToCharArray());

            // The first component contains the word MERGEFIELD, 
            // followed by the merge field label, separated by a space

            // Set the name
            if (components.Length > 1)
            {
                string[] subComponents = components[1].Split(" ".ToCharArray());

                name = subComponents[0].Trim();

                if (subComponents.Length > 1)
                {
                    filter = "";

                    for (int i = 1; i < subComponents.Length; i++)
                    {
                        if(i > 1)
                            filter += " ";

                        filter += subComponents[i].Trim();
                    }
                }
            }
        }

        private IEnumerable<MergeFieldSwitch> ParseMergeFieldSwitches(string[] components)
        {
            List<MergeFieldSwitch> switches = new List<MergeFieldSwitch>();
            string component;
            string name;
            string value;
            int valueIndex;

            if (components.Length > 1)
            {
                for (int i = 1; i < components.Length; i++)
                {
                    component = components[i].Trim();

                    // Does this component contain a value?
                    valueIndex = component.IndexOf(" ");

                    if(valueIndex > -1)
                    {
                        name = component.Substring(0, valueIndex).Trim();
                        value = component.Substring(valueIndex + 1).Trim();
                    }
                    else
                    {
                        name = component.Trim();
                        value = "";
                    }

                    switches.Add(new MergeFieldSwitch(name, value));
                }
            }

            return switches;
        }

        private MergeRegion GetRegion(MergeField regionStart, MergeRegion parentRegion)
        {
            MergeRegion region = null;

            if (regionStart.Name != null)
            {
                if (regionStart.Name.Contains("TableStart:"))
                {
                    region = new MergeRegion();
                    region.Type = MergeRegionType.Table;
                }
                else if (regionStart.Name.Contains("IfStart:"))
                {
                    region = new MergeRegion();
                    region.Type = MergeRegionType.Conditional;
                }
            }

            if (region != null)
            {
                string regionName;
                string regionFilter;
                
                this.ParseRegionValue(regionStart.Name, out regionName, out regionFilter);

                region.Name = regionName;
                region.Filter = regionFilter;

                // Ensure the region filter is valid
                if (this.ValidateRegionFilter(region, parentRegion))
                {
                    // Get the associated TableEnd merge field
                    string regionEndName;

                    if (region.Type == MergeRegionType.Table)
                    {
                        regionEndName = "TableEnd";
                    }
                    else
                    {
                        regionEndName = "IfEnd";
                    }

                    MergeField regionEnd = this.FindMergeFieldByText(regionEndName + ":" + region.Name);

                    if (regionEnd != null)
                    {
                        // Get the nodes contained in this region
                        List<Node> regionNodes = this.GetRegionNodes(region, regionStart, regionEnd);

                        if (regionNodes.Count > 0)
                        {
                            region.Start = regionNodes[0];
                            region.End = regionNodes[regionNodes.Count - 1];
                            region.Nodes = regionNodes;
                        }
                        else
                        {
                            this.ErrorList.Add(new MergeError(
                                MergeErrorType.RegionEmpty,
                                region.Name,
                                "The region " + region.Name + " is empty."));

                            region = null;
                        }
                    }
                    else
                    {
                        this.ErrorList.Add(new MergeError(
                            MergeErrorType.MissingRegionEnd,
                            region.Name,
                            "Missing " + regionEndName + " merge field for region " + region.Name + "."));

                        region = null;
                    }
                }
                else
                {
                    region = null;
                }
            }

            return region;
        }

        private bool ValidateRegionFilter(MergeRegion region, MergeRegion parentRegion)
        {
            bool validFilter = true;

            if (region.Filter != null)
            {
                MergeRegionData regionData;
                DataTable data = null;

                if (region.Type == MergeRegionType.Conditional)
                {
                    // Build conditional DataTable for validation
                    if (parentRegion != null)
                    {
                        regionData = this.GetRegionData(parentRegion.Name);
                        DataRow dataRow = null;

                        if (regionData != null && regionData.Data.Rows.Count > 0)
                        {
                            dataRow = regionData.Data.Rows[0];
                        }
                        else if(regionData != null)
                        {
                            // There is no data for this region, so create a new row
                            dataRow = regionData.Data.NewRow();
                        }

                        if (dataRow != null || this.DocumentData != null)
                        {
                            data = this.GetConditionalDataTable(dataRow);
                        }
                    }
                }
                else
                {
                    regionData = this.GetRegionData(region.Name);

                    if (regionData != null)
                    {
                        data = regionData.Data;
                    }
                }

                // Validate the filter against the data source
                if (data != null)
                {
                    try
                    {
                        data.Select(region.Filter);
                    }
                    catch (Exception e)
                    {
                        this.ErrorList.Add(new MergeError(
                            MergeErrorType.InvalidRegionFilter,
                            region.Name,
                            "The region " + region.Name + " has an invalid filter expression: " +
                            e.Message));

                        validFilter = false;
                    }
                }
            }

            return validFilter;
        }

        private MergeField FindMergeFieldByText(string text)
        {
            NodeList fields = this.Document.SelectNodes("//FieldStart");
            MergeField mergeField = null;
            MergeField foundField;

            foreach (Node field in fields)
            {
                foundField = this.GetMergeField((FieldStart)field);

                if (foundField.Name == text)
                {
                    mergeField = foundField;
                    break;
                }
            }

            return mergeField;
        }

        private List<Node> GetRegionNodes(MergeRegion region, MergeField regionStart, MergeField regionEnd)
        {
            List<Node> nodes = new List<Node>();

            Cell startParentCell = this.GetNodeParentCell(regionStart.Start);
            Cell endParentCell = this.GetNodeParentCell(regionEnd.End);

            // Is the region start or end within a table cell, and are they in different cells?
            if ((startParentCell != null && endParentCell != null) &&
                (startParentCell != endParentCell))
            {
                // Yes, do both rows belong to the same table?
                if (startParentCell.ParentNode.ParentNode != startParentCell.ParentNode.ParentNode)
                {
                    // No, so return an error
                    this.ErrorList.Add(new MergeError(
                        MergeErrorType.RegionStartAndEndDifferentParents,
                        region.Name,
                        "The start & end merge fields for the region " + region.Name + " are " +
                        "in different tables."));
                }
                else
                {
                    // Add the rows to the node list
                    nodes.Add(startParentCell.ParentNode);

                    if (endParentCell.ParentNode != startParentCell.ParentNode)
                    {
                        // Add all rows between the start & end
                        Node nextRow = startParentCell.ParentNode;

                        do
                        {
                            nextRow = nextRow.NextSibling;
                            nodes.Add(nextRow);
                        }
                        while (nextRow != endParentCell.ParentNode);
                    }
                }
            }
            else
            {
                // Region does not sit in a table, ensure the start & end sit at the same level in
                // the document tree
                Node startParentNode = regionStart.Start;
                Node endParentNode = regionEnd.End;

                // If the start & end nodes belong to different parents, it is common that they 
                // are both in their own Paragraph tags
                if (startParentNode.ParentNode != endParentNode.ParentNode)
                {
                    startParentNode = this.GetNodeParentParagraph(regionStart.Start);
                    endParentNode = this.GetNodeParentParagraph(regionEnd.Start);
                }
                else
                {
                    // Since the start & end are the actual merge fields, we need to change the
                    // start and end to be their siblings, since the merge fields will be removed
                    if (regionStart.End.NextSibling != regionEnd.Start)
                    {
                        startParentNode = regionStart.End.NextSibling;
                        endParentNode = regionEnd.Start.PreviousSibling;
                    }
                }

                if (startParentNode.ParentNode != endParentNode.ParentNode)
                {
                    // No, so return an error
                    this.ErrorList.Add(new MergeError(
                        MergeErrorType.RegionStartAndEndDifferentParents,
                        region.Name,
                        "The start & end merge fields for the region " + region.Name + " are " +
                        "in different levels of the document."));
                }
                else
                {
                    // Add all nodes between the merge fields to the node list
                    Node currentNode = startParentNode;

                    while (currentNode != endParentNode && currentNode != null)
                    {
                        nodes.Add(currentNode);
                        currentNode = currentNode.NextSibling;
                    }

                    if(currentNode != null)
                        nodes.Add(currentNode);
                }
            }

            // Finally, remove the merge fields as they are no longer needed, and they can cause
            // an infinite loop when recursing through regions
            this.RemoveMergeField(regionStart);
            this.RemoveMergeField(regionEnd);

            return nodes;
        }

        private Cell GetNodeParentCell(Node node)
        {
            Cell cell = null;

            while (node.ParentNode != null)
            {
                if (node.ParentNode.GetType() == typeof(Cell))
                {
                    cell = (Cell)node.ParentNode;
                    break;
                }

                node = node.ParentNode;
            }

            return cell;
        }

        private CompositeNode GetNodeParentParagraph(Node node)
        {
            CompositeNode para = null;

            while (node.ParentNode != null)
            {
                if (node.ParentNode.GetType() == typeof(Paragraph))
                {
                    para = node.ParentNode;
                    break;
                }

                node = node.ParentNode;
            }

            return para;
        }
    }
}